package cn.t.keycloak.demo.mfa.sms.credential;

import cn.t.keycloak.demo.mfa.sms.credential.dto.SmsCredentialData;
import cn.t.keycloak.demo.mfa.sms.util.SmsCache;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.common.util.Time;
import org.keycloak.credential.*;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.util.JsonSerialization;

import java.io.IOException;
import java.util.Objects;
import java.util.Optional;

/**
 * CredentialInputValidator接口告知keycloak这个类可以用于校验认证器认证
 * @author 陶敏麒
 * @date 2023/11/15 16:18
 */
@Slf4j
public class SmsCredentialProvider implements CredentialProvider<SmsCredentialModel>, CredentialInputValidator {

    private final KeycloakSession session;

    public SmsCredentialProvider(KeycloakSession session) {
        this.session = session;
    }

    /**
     * 指向扩展类的类型
     */
    @Override
    public String getType() {
        return SmsCredentialModel.TYPE;
    }

    /**
     * 创建用户凭证<br>
     * 这些方法会调用KeycloakSession的 userCredentialManager对象<br>
     * 这个对象知道如何读取或编辑凭证，比如通过本地存储或联合存储
     */
    @Override
    public CredentialModel createCredential(RealmModel realmModel, UserModel userModel, SmsCredentialModel smsCredentialModel) {
        log.info("开始给用户[{}]创建手机号凭证", userModel.getUsername());
        if (smsCredentialModel.getCreatedDate() == null) {
            smsCredentialModel.setCreatedDate(Time.currentTimeMillis());
        }
        return userModel.credentialManager().createStoredCredential(smsCredentialModel);
    }

    /**
     * 删除凭证
     */
    @Override
    public boolean deleteCredential(RealmModel realmModel, UserModel userModel, String credentialId) {
        return userModel.credentialManager().removeStoredCredentialById(credentialId);
    }

    /**
     * 从用户凭证中创建拓展凭证
     */
    @Override
    public SmsCredentialModel getCredentialFromModel(CredentialModel credentialModel) {
        return SmsCredentialModel.createFromCredentialModel(credentialModel);
    }

    /**
     * 类型、认证其类别、展示名称以及可移除项目
     */
    @Override
    public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext credentialTypeMetadataContext) {
        return CredentialTypeMetadata.builder()
                .type(this.getType())
                .category(CredentialTypeMetadata.Category.TWO_FACTOR) // 二次认证
                .displayName(SmsCredentialProviderFactory.PROVIDER_ID) // 显示名称
                .helpText("手机验证码验证")
                .createAction(SmsCredentialProviderFactory.PROVIDER_ID) // 谁创建这个认证,用户可以使用这个id创建新的凭证
                .removeable(true) // 用户是否可以自主移除这个认证
                .build(this.session);
    }

    /**
     * 自定义查询(是否已经配置过手机号了)
     */
    public boolean hasSmsValidatePhone(UserModel userModel) {
        return userModel.credentialManager().getStoredCredentialsByTypeStream(SmsCredentialModel.TYPE).findAny().isPresent();
    }

    /**
     * 是否支持本类型
     */
    @Override
    public boolean supportsCredentialType(String credentialType) {
        return Objects.equals(credentialType, this.getType());
    }

    /**
     * 当前用户是否配置了这个校验
     */
    @Override
    public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String credentialType) {
        log.info(">>>>校验是否配置了SMS认证器,当前类型:{},用户:{}", credentialType, userModel.getUsername());
        if (!this.supportsCredentialType(credentialType)) return false;
        // 当前用户是否配置了这个校验
        return userModel.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent();
    }

    /**
     * 这个方法检测指定域下的指定用户的凭证是否有效<br>
     * 认证器需要校验用户输入数据时调用此方法<br>
     * 这里只要校验手机号码是否存在<br>
     * 这个方法由认证器调用
     */
    @Override
    public boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput credentialInput) {
        log.info(">>>>>开始校验用户的手机号码是否存在");
        if (!(credentialInput instanceof UserCredentialModel)) {
            log.warn("当前校验的输入不是用户认证类型");
            return false;
        }
        if (!Objects.equals(credentialInput.getType(), this.getType())) {
            return false;
        }
        // 从用户信息中获取手机号
        // String phoneNumber = userModel.getFirstAttribute("phoneNumber");
        // 从凭证信息中获取手机号
        String phoneNumber = Optional.ofNullable(userModel.credentialManager().getStoredCredentialById(credentialInput.getCredentialId()))
                .map(c -> {
                    try {
                        return JsonSerialization.readValue(c.getCredentialData(), SmsCredentialData.class);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }).map(SmsCredentialData::getPhoneNumber).orElse("");
        if (phoneNumber.isBlank()) {
            log.warn("当前用户:{}的手机号凭证(phoneNumber字段)不存在", userModel.getUsername());
            // 用户手机号不存在
            return false;
        }
        // 获取输入(在认证器已经将code放到输入中了,使用challengeResponse获取即可)
        String smsCode = credentialInput.getChallengeResponse();
        if (smsCode == null || smsCode.isBlank()) {
            log.warn("当前用户[手机号:{}, 用户名:{}]未输入验证码", phoneNumber, userModel.getUsername());
            return false;
        }

        // 校验验证码(验证码放在缓存里面即可)
        String code = SmsCache.getCode(realmModel.getName(), phoneNumber);
        boolean res = Objects.equals(code, smsCode);
        if (res) {
            SmsCache.cleanCode(realmModel.getName(), phoneNumber);
        }
        return res;
    }
}
